package application;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import jeopardy.common.protocol.game.play.GameRecordMsg;
import jeopardy.common.protocol.game.play.NewQuestionMsg;
import jeopardy.common.protocol.game.play.QuestionAnswerMsg;
import jeopardy.server.ServerConsts;
import jeopardy.server.db.H2ServerDb;

import org.apache.log4j.Logger;

public class Game implements Runnable{
	
	private static Logger log = Logger.getLogger(Game.class);
	
	private boolean gameStarted = false;
	
	private BlockingQueue<UserAnswer> answers = new LinkedBlockingQueue<UserAnswer>();
	
	private List<QuestionAnswerMsg> playedRounds = new LinkedList<QuestionAnswerMsg>();
	private GameRecordMsg finalRecord;
	
	private Map<String, LoginScreenLogic> activeClients = new HashMap<String, LoginScreenLogic>();
	private List<String> initialClients = new LinkedList<String>();
	
	private GameManager manager = null;
	private String gid = null;
	
	private BlockingQueue<Serializable> toSendQueue = new LinkedBlockingQueue<Serializable>();

	public Game(String gid, GameManager gm, H2ServerDb db){
		this.gid = gid;
		this.manager = gm;
		
	}
	
	public boolean joinGame(LoginScreenLogic c){
		synchronized(activeClients){
			if(!gameStarted && !activeClients.containsKey(c.getUserName())){
				activeClients.put(c.getUserName(), c);
				initialClients.add(c.getUserName());
				c.joinGame(this);
				return true;
			}else if(initialClients.contains(c.getUserName()) && !activeClients.containsKey(c.getUserName())){
				activeClients.put(c.getUserName(), c);
				c.joinGame(this);
				return true;
			}else{
				return false;
			}
		}
	}
	
	public void leaveGame(LoginScreenLogic c){
		synchronized (activeClients) {
			activeClients.remove(c.getUserName());
		}
	}
	
	public String getGid(){
		return gid;
	}
	
	public void submitAnswer(UserAnswer ua){
		answers.offer(ua);
	}
	
	@Override
	public void run() {
		try{
			log.info("[" + gid + "] Starting...");
			initUsers();
			gameStarted = true;
			notifyStart();
			Thread.sleep(ServerConsts.GAME_START_WAIT_MILLI);	
		
			if(!waitUsers()){
				notifyFinish();
			}else{
				int roundsPlayed = 0;
				while(roundsPlayed < ServerConsts.ROUNDS_PER_GAME && waitUsers()){
					playRound();
					roundsPlayed++;
				}
				notifyFinish();
			}
			
		}catch(Exception e){
			
		}finally{
			manager.reportFinished(finalRecord);
		}
	}
	
	private void playRound(){
		 
		List<String> eligibleUsers = new LinkedList<String>();
		synchronized(activeClients){
			Iterator<LoginScreenLogic> clients = activeClients.values().iterator();
			while(clients.hasNext()){
				eligibleUsers.add(clients.next().getUserName());
			}
		}
		
		NewQuestionMsg q = new NewQuestionMsg("question", "answer", 60000L, eligibleUsers.toArray(new String[0]), 10);
		
		synchronized(activeClients){
			Iterator<LoginScreenLogic> clients = activeClients.values().iterator();
			while(clients.hasNext()){
				LoginScreenLogic c = clients.next();
				c.tellQuestion(q);
			}
		}
		
		boolean answered = false;
		
		long playStart = System.currentTimeMillis();
		long playUntil = playStart + q.getTimeToPlayMillis();
		
		String winner = null;
		long timeTook = 0L;
		
		while(!answered && System.currentTimeMillis() < playUntil && !eligibleUsers.isEmpty()){
			try{
				UserAnswer ua = answers.poll(10, TimeUnit.MILLISECONDS);
				
				timeTook = System.currentTimeMillis() - playStart;
				
				if(eligibleUsers.contains(ua.getUser())){
					eligibleUsers.remove(ua.getUser());
					if(q.getAnswer().equalsIgnoreCase(ua.getAnswer())){
						answered = true;
						winner = ua.getUser();
					}
					synchronized(activeClients){
						Iterator<LoginScreenLogic> clients = activeClients.values().iterator();
						while(clients.hasNext()){
							clients.next().tellQuestionStatus(ua.getAnswer(), ua.getUser(), timeTook, eligibleUsers.toArray(new String[0]));
						}
					}
				}
			}catch(Exception e){
				log.error("WTFERY", e);  //TODO no curse words
			}
		}
		
		QuestionAnswerMsg qa = new QuestionAnswerMsg(q.getQuestion(), q.getAnswer(), winner, q.getPoints(), timeTook);
		synchronized(activeClients){
			Iterator<LoginScreenLogic> clients = activeClients.values().iterator();
			while(clients.hasNext()){
				clients.next().tellQuestionFinished(qa);
			}
		}
		answers.clear();
		playedRounds.add(qa);
	}
	
	private void notifyFinish(){
		finalRecord = new GameRecordMsg(gid, initialClients.toArray(new String[0]), playedRounds.toArray(new QuestionAnswerMsg[0]));
		synchronized(activeClients){
			Iterator<LoginScreenLogic> clients = activeClients.values().iterator();
			
			while(clients.hasNext()){
				clients.next().tellFinished(finalRecord);
			}
		}
	}
	
	private void notifyStart(){
		synchronized(activeClients){
			Iterator<LoginScreenLogic> clients = activeClients.values().iterator();
			while(clients.hasNext()){
				clients.next().tellGetReady(ServerConsts.GAME_START_WAIT_MILLI, initialClients);
			}
		}
		
	}
	
	/**
	 * Wait for at least 2 users, and start the timer
	 * @return
	 */
	private void initUsers(){
		while(activeClients.size() < 2){
			try{
				Thread.sleep(1000);
			}catch(Exception e){
				log.error("WTFERY", e);;
			} //TODO log here, support more than 2 users?
		}
		synchronized(activeClients){
			Iterator<String> usersIt = activeClients.keySet().iterator();
			while(usersIt.hasNext()){
				initialClients.add(usersIt.next());
			}
			gameStarted = true;
		}
	}
	
	/**
	 * Wait until all the users are here to play.
	 * @return true/false indicating whether all the users are here
	 */
	private boolean waitUsers(){
		long waited = 0;
		long waitStep = 1000;
		while(activeClients.size() != initialClients.size() && waited < ServerConsts.MAX_USER_WAIT_TIME_MILLIS){
			try{
				Thread.sleep(waitStep);
				waited = waited + waitStep;
			}catch(Exception e){
				log.error("WTFERY", e);
			}//TODO no curse words
		}
		return activeClients.size() > 1;
	}
}